const SocketWrapper =require('./lib/wsocket')
const Protocol = require('./lib/pomelo-protocol')
const Protobuf = require('./lib/pomelo-protobuf')

import Logger from './logger'

import Connectable from './connectable'

/**
 *  DONT MODIFY ======= 不要随便变更，如果需要，请事先联系 248751
 * 此是底层与远程websocket服务链接类
 * 作用：
 * 1、创建链接
 * 2、发送protobuf握手协议，就是告知服务端需要使用什么协议传输，并从服务获取proto格式协议列表
 * 3、维持心跳
 * 4、proto协议传输压缩编码、解码
 * 5、使用延迟对象，确保发送消息都是在链接握手完成之后。这步比较重要，确保了调用的便捷性。
 * 注意点：
 * 发送路由给服务端，但服务端并未返回路由。根据路由找回调方法，此类做了映射处理。
 */
export default class Connector {

    private opts: {} = {};
    private logger = Logger;

    private SDK_PROTO_KEY: string = "_sdk_protos_";

    private protobuf: any;

    private dict: any;

    private RES_OK: number = 200;
    private RES_FAIL: number = 500;
    private RES_OLD_CLIENT: number = 501;


    private heartbeatInterval = 0;
    private heartbeatTimeout = 0;

    protected _socket: WebSocket;

    protected data: any;

    private reqId: number = 0;

    private host: string;
    private port: string;
    private ssl: string;

    private promise: any;
    private resolveFn: any;
    private rejectFn: any;

    private callbacks: any;
    private routeMap: any;

    private prepareWebSocket: any;

    private event: any;

    private master: Connectable;

    /**
     * 
     * @param master 创建Connector的对象
     * @param host   
     * @param port 
     * @param ssl 
     * @param event 
     */
    constructor(master: any, host: string, port: string, opts: any) {
        this.master = master;
        this.host = host;
        this.port = port;

        opts = opts || {};
        this.opts = opts;
        if (opts.logger) {
            this.logger = opts.logger;
        }
        this.ssl = opts.ssl || '';
        this.event = opts.event;

        this.init();
        this.initSocket();
    }

    private init() {
        var self = this;
        this.protobuf = Protobuf;
        //加载缓存的protos
        if (typeof (localStorage) !== 'undefined') {
            var _sProto = localStorage.getItem(this.SDK_PROTO_KEY);
            this.data = JSON.parse(_sProto);
        }
        this.callbacks = {};
        this.routeMap = {};
    }

    /**暂时与调用方的Flag一致 */
    private genId() {
        return this.master;
    }


    private initSocket() {
        var self = this;
        this.prepareWebSocket = false;
        let win = ("object" == typeof window) ? window : null;
        let proctocol = (win&&win.location)?win.location.protocol:'';
        if ('https:' == proctocol) {
            this.ssl = 'SSL';
        }
        SocketWrapper.getInstance(this.host, this.port, this.ssl).bind({
            onmessage: self.onMessgae.bind(self),
            onerror: self.onError.bind(self),
            onopen: self.onOpen.bind(self),
            onclose: self.onClose.bind(self)
        }, function (_sct, _reuse) {
            self.promise = new Promise(function (resolve, reject) {
                self.callbacks['ALL_READY_FOR_REQUEST'] = resolve;
            });
        });
    }

    /**
     * 外部发送消息之前，都需要调用此方法，主要是确保protobuf协议初始化完成；如果未完成，则等待完成. 已经完成，则立即唤醒新加入的then方法.
     * 此方法类似 jQ 中的 ready方法。
     * 调用此方法时，还未初始化，则返回Pomelo的promise。如果已经初始化完成，则返回新的Promise。 好处是，每个调用都有新Promise作用域。
     */
    private ready() {
        var self = this;
        var _promise = null;
        if (this.prepareWebSocket) {
            var so = setTimeout(function () {
                self.alreadyForRequest();
                clearTimeout(so);
            }, 0)
            _promise = this.promise.then(function () {
                return Promise.resolve(null);
            })
        } else {
            _promise = this.promise;
        }
        return _promise;
    }

    /**
     * 这个方法是当握手完成（protobuf协议初始化完成）后调用，是的延迟promise对象去执行后续方法
     * 
     */
    private alreadyForRequest() {
        this.logger.debug('request immediately', this.prepareWebSocket, this.callbacks['ALL_READY_FOR_REQUEST'], this.master);
        if (this.callbacks['ALL_READY_FOR_REQUEST']) {
            this.prepareWebSocket = true;
            this.callbacks['ALL_READY_FOR_REQUEST']();
        }
    }



    /**
     * 接收服务端的错误信息接口
     * @param error 
     */
    private onError(error) {
        this.clearTimeOut();
        if (!!this.event) {
            this.event.emit("WS_ERROR", { sponsor: this.master, error: error });
        }
    }

    /**
     * 
     * @param evt 处理握手协议之类的
     * @param _socket 
     * @param reuse 
     */
    private onOpen(evt, _socket, reuse) {
        this._socket = _socket;
        this.logger.debug(evt, _socket);
        //非重用，则需要握手协议之类的
        if (!reuse) {
            this.sendHandShake();
        }
    }

    /**
     * 接收服务端的消息接口
     * 首次连接
     * 1、new 对象（new websocket） ， 服务端返回open消息； 
     * 2、客户端在onOpen上，发送HANDSHAKE消息
     * 3、服务端发送handshake的message消息，在onMessage上，客户端解析完成就初始化，具备与protobuf通信的能力
     * 4、常规发送TYPE_DATA数据
     * 
     * 第二次连接
     * 1、new 对象（如果存在从缓存中获取，否则走首次连接），
     * @param evt
     */
    private onMessgae(evt) {
        //try {
        var msg = Protocol.Package.decode(evt['data']);
        switch (msg.type) {
            case Protocol.Package.TYPE_HANDSHAKE:
                this.initProtoBuf(msg.body);
                this.alreadyForRequest()
                break;
            case Protocol.Package.TYPE_HEARTBEAT:
                this.heartBeat()
                break;
            case Protocol.Package.TYPE_DATA:
                this.onData(msg.body)
                break;
            case Protocol.Package.TYPE_KICK:
                break;
            default: break;
        }
        /*} catch (e) {
            this.logger.error('ERROR', e);
            throw new SyntaxError(e)
        }*/
    }

    /**处理数据 */
    private onData(data) {
        //probuff decode        
        var msg;
        try {
            msg = Protocol.Message.decode(data);
            //填充route
            if (msg.id > 0) {
                msg.route = this.routeMap[msg.id + '_RM'];
                delete this.routeMap[msg.id + '_RM'];
                if (!msg.route) {
                    return;
                }
            }
            msg.body = this.deCompose(msg);
        } catch (e) {
            this.logger.error('failed decode msg', e);
            return;
        }
        if (!msg.id) {
            this.logger.debug("receive notify message ", msg);
            // server push message
            if (!!this.event) {
                this.event.emit(msg.route, msg.body);
                this.event.emit('S_NOTIFY', { sponsor: this.master, route: msg.route, body: msg.body });
            }
            return;
        }

        this.logger.debug("receive request message ", msg);

        //if have a id then find the callback function with the request
        var cb = this.callbacks[msg.id + '_S'];
        if (!!cb) {
            delete this.callbacks[msg.id + '_S'];
            cb(msg.body)
        } else {
            this.logger.error('unknown response', msg)
        }
    }

    private deCompose(msg) {
        var protos = !!this.data.protos ? this.data.protos.server : {};
        var abbrs = this.data.abbrs;
        var route = msg.route;

        //Decompose route from dict
        if (msg.compressRoute) {
            if (!abbrs[route]) {
                return {};
            }

            route = msg.route = abbrs[route];
        }
        if (this.protobuf) {
            try {
                var m = this.protobuf.decode(route, msg.body);
                if (!!m) {
                    return m;
                }
            }
            catch{
            }
        }
        return JSON.parse(Protocol.strdecode(msg.body));
    };

    private lastBeat: number;
    private HeartBeatId: any;

    /**心跳 */
    private heartBeat() {

        if (!this.heartbeatInterval) {
            // no heartbeat
            return;
        }

        var self = this;

        this.clearTimeOut();

        this.HeartBeatId = setTimeout(function () {
            var obj = Protocol.Package.encode(Protocol.Package.TYPE_HEARTBEAT);
            self.send(obj);
            //重置
            self.lastBeat = (new Date()).getTime();
        }, this.heartbeatInterval)
    }

    /**初始化ProtoBuf协议 ， 是服务端下发协议，客户端初始化协议实例 */
    private initProtoBuf(_data) {
        try {
            var data = JSON.parse(Protocol.strdecode(_data));
            this.logger.debug(data);
            if (data.code === this.RES_OLD_CLIENT) {
                if (!!this.event) {
                    this.event.emit('ERROR', { sponsor: this.master, error: 'client version not fullfill' });
                }
                return;
            }

            if (data.code !== this.RES_OK) {
                if (!!this.event) {
                    this.event.emit('ERROR', { sponsor: this.master, error: 'handshake fail' });
                }
                return;
            }

            // initData(data);\
            if (!data || !data.sys) {
                return;
            }

            // handshakeInit(data);
            if (data.sys && data.sys.heartbeat) {
                this.heartbeatInterval = data.sys.heartbeat * 1000;   // heartbeat interval
                this.heartbeatTimeout = this.heartbeatInterval * 2;        // max heartbeat timeout
            } else {
                this.heartbeatInterval = 0;
                this.heartbeatTimeout = 0;
            }


            this.data = this.data || {};
            var dict = data.sys.dict;
            var protos = data.sys.protos;
            //Init compress dict
            if (dict) {
                this.data.dict = dict;
                this.data.abbrs = {};
                for (var route in dict) {
                    this.data.abbrs[dict[route]] = route;
                }
                this.data.dictVersion = data.sys.dictVersion;
            }

            //Init protobuf protos
            if (protos && protos.proto) {
                this.data.protoVersion = protos.version;
                this.data.protos = protos;
            }

            if (this.protobuf && this.data.protos && this.data.protos.proto)
                this.protobuf.init(this.data.protos);

            //字典与proto任意发生变化，都需要重新写入本地缓存
            if (!!dict || !!protos) {
                if (typeof (localStorage) !== 'undefined') {
                    localStorage.setItem(this.SDK_PROTO_KEY, JSON.stringify(this.data));
                }
            }

            var obj = Protocol.Package.encode(Protocol.Package.TYPE_HANDSHAKE_ACK);
            this.send(obj);
        } catch (e) {
            this.logger.error('failed init protobuf', e);
        }
    }

    /**
     *  发送handshake与服务端做协议协商
     */
    private sendHandShake() {
        var JS_WS_CLIENT_TYPE = 'js-websocket';
        var JS_WS_CLIENT_VERSION = '0.0.1';
        var handshakeBuffer = {
            'sys': {
                type: JS_WS_CLIENT_TYPE,
                version: JS_WS_CLIENT_VERSION
            },
            'user': {}
        };
        if (this.data) {
            handshakeBuffer.sys['protoVersion'] = this.data.protoVersion;
            handshakeBuffer.sys['dictVersion'] = this.data.dictVersion;
        }
        var obj = Protocol.Package.encode(Protocol.Package.TYPE_HANDSHAKE, Protocol.strencode(JSON.stringify(handshakeBuffer)));
        this.send(obj);
    }

    private send(packet) {
        this._socket.send(packet.buffer);
    }


    /**
     * 关闭连接接口
     */
    private onClose(event) {
        this.clearTimeOut();
        if (!!this.event) {
            this.event.emit("WS_CLOSE", { sponsor: this.master, error: "websocket closed" });
        }

        if (!!this.master) {
            this.master.close();
        }
    }

    private clearTimeOut() {
        if (this.HeartBeatId) {
            clearTimeout(this.HeartBeatId);
            this.HeartBeatId = null;
        }
    }

    /**
     * 关闭方法
     */
    close() {
        this._socket.close();
    }

    /**
     * 发送消息封装，1、protobuf对消息内容encode 2、使用protocol把消息在此strencode
     * 3、使用Message encode ，主要是封包消息头（是什么类型的消息）
     * 4、使用Packet封包，增加包头（是什么包）
     * @param reqId 
     * @param route 
     * @param msg 
     */
    private sendMessage(reqId, route, msg) {
        var type = reqId ? Protocol.Message.TYPE_REQUEST : Protocol.Message.TYPE_NOTIFY;
        this.logger.debug('sendMessage ' + reqId, reqId, route, msg, type)
        //compress message by protobuf
        var mbak = msg;
        if (this.protobuf) {
            try {
                msg = this.protobuf.encode(route, msg);
            }
            catch{
                msg = null;
            }
        }
        if (!msg) {
            msg = Protocol.strencode(JSON.stringify(mbak));
        }
        var compressRoute = 0;
        if (this.data.dict && this.data.dict[route]) {
            route = this.data.dict[route];
            compressRoute = 1;
        }
        msg = Protocol.Message.encode(reqId, type, compressRoute, route, msg);
        var packet = Protocol.Package.encode(Protocol.Package.TYPE_DATA, msg);
        this.send(packet);
    }

    request(route, msg) {
        var self = this;
        return this.ready().then(function () {
            return self.requestInner(route, msg);
        })
    }

    notify(route, msg) {
        var self = this;
        return this.ready().then(function () {
            self.sendMessage(0, route, msg);
        })
    }

    private requestInner(route, msg): Promise<any> {
        if (arguments.length === 2 && typeof msg === 'function') {
            msg = {};
        } else {
            msg = msg || {};
        }
        route = route || msg.route;
        if (!route) {
            return;
        }

        this.reqId++;
        var self = this;
        var promise = new Promise(function (resolve, reject) {
            self.callbacks[self.reqId + '_S'] = resolve;
            // self.callbacks[self.reqId + '_ERROR'] = reject;
            self.routeMap[self.reqId + '_RM'] = route;
        });
        this.sendMessage(this.reqId, route, msg);
        return promise;
    }
}